<?php

namespace libraries;
use enums\CachePrefixEnums;
use enums\ServiceEnums;
!defined( 'APP_ROOT' ) && exit( 'Direct Access Deny!' );

/**
 *
 * @author fzq
 * @comment 以Redis为基础实现的分布式资源锁 需要script的支持
 * @date 2016-11-25
 */
class DLock //implements \Phalcon\DI\InjectionAwareInterface
{
	/**
	 * 强制初始化（强制覆盖原有值，自行处理善后）
	 */
	const INIT_FORCE = 1;
	
	/**
	 * 非强制初始化 （有则返回剩余资源量，无则直接初始化）
	 */
	const INIT_FORCE_NONE = 2;
	
	/**
	 * 默认资源数量
	 */
	const DEFAULT_INIT_RES_SIZE = 20;
	
	/**
	 * 默认的锁定时间
	 */
	const DEFAULT_LOCK_SECONDS = 100;
	
	const DEFAULT_APPENDIX = '_lockers';
	
	private static $_instance = null;
	
	private $_redis = null;
	private $_di = null;
	
	/**
	 * 强制初始化
	 */
	private $INIT_FORCE = 1;
	
	/**
	 * 不强制初始化
	 */
	private $INIT_FORCE_NONE = 2;
	
	/**
	 * 返回成功
	 */
	private $ERR_SUCCESS = 1;
	
	/**
	 * 返回成功
	 */
	const ERR_SUCCESS = 1;

	
	/**
	 * 参数错误
	 */
	private $ERR_PARAMETERS = -1;
	
	/**
	 * 参数错误
	 */
	const ERR_PARAMETERS = -1;
	
	/**
	 * 未初始化
	 */
	private $ERR_NONE_INIT = -2;
	/**
	 * 未初始化
	 */
	const ERR_NONE_INIT = -2;
	
	/**
	 * 初始化参数错误
	 */
	private $ERR_INIT_PARAMETERS = -3;
	/**
	 * 初始化参数错误
	 */
	const ERR_INIT_PARAMETERS = -3;
	
	/**
	 * 所需资源大于总资源量
	 */
	private $ERR_EXCEED = -4;
	
	/**
	 * 所需资源大于总资源量
	 */
	const ERR_EXCEED = -4;
	
	/**
	 * 资源不够加锁失败
	 */
	private $ERR_INSUFFICIENT = -5;
	/**
	 * 资源不够加锁失败
	 */
	const ERR_INSUFFICIENT = -5;
	
	/**
	 * 无申请者
	 */
	private $ERR_NO_APPLICANTS = -6;
	/**
	 * 无申请者
	 */
	const ERR_NO_APPLICANTS = -6;
	
	/**
	 * 消费失败 可能是已失去资源锁或是其他错误
	 */
	private $ERR_CONSUME = -7;
	/**
	 * 消费失败 可能是已失去资源锁或是其他错误
	 */
	const ERR_CONSUME = -7; 
	
	/**
	 * 资源已耗尽或是未初始化
	 */
	private $ERR_NO_RESOURCE_LEFT = -8;
	/**
	 * 资源已耗尽或是未初始化
	 */
	const ERR_NO_RESOURCE_LEFT = -8;
	
	/**
	 * 剩余资源量
	 */
	private $LEFT_APPENDIX = '_left';

	
	public function __construct( $di ) 
	{
		$this->_di = $di;
		
		$this->_redis = $di[ ServiceEnums::SERVICE_REDIS_DB_LOCK ];
	}
	
	public static function getInstance( $di )
	{
		if( !self::$_instance )
		{
			self::$_instance = new DLock( $di );
		}
		
		return self::$_instance;
	}
	
	/**
	 * status: done
	 * 初始化锁
	 * @param string $strLockName 资源锁名
	 * @param int $iResID 资源ID
	 * @param int $iBase 初始加锁时的资源
	 * 
	 * return false：错误 int:未消费的资源数
	 */
	public function initLock( string $strLockName, int $iResID, int $iBase = self::DEFAULT_INIT_RES_SIZE, $iForce = self::INIT_FORCE_NONE )
	{
		if( $iResID <= 0 || 
			$iBase <= 0 || 
			!$strLockName || 
			( $strLockName = trim( $strLockName ) ) == '' )
		{
			return self::ERR_PARAMETERS;
		}
		
		$strLock = CachePrefixEnums::LOCK_RES_PREFIX .  $strLockName;
// 		$strListApplicantKeys = CachePrefixEnums::LOCK_RES_LIST_KEYS_PREFIX . $strLockName . '_' . $iResID;//用于获取某个资源所有的持锁者ID集
		
		$strScript = <<< EOS
	
			local iCnt = redis.call( 'hGet', '$strLock', '$iResID$this->LEFT_APPENDIX' );
			
			if( $iForce == $this->INIT_FORCE_NONE ) then
				if iCnt then
					return iCnt;
				end
			end
			
			redis.call( 'hSet', '$strLock', $iResID, $iBase );
			redis.call( 'hSet', '$strLock', '$iResID$this->LEFT_APPENDIX', $iBase );
			return true;
		
EOS;
		return $this->_redis->eval( $strScript );
	}
	
	/**
	 * @comment 取剩余的资源数量
	 * 剩余的资源数量 = 总资源量 - 消费的资源量
	 * @param string $strLockName
	 * @param int $iResID
	 * 
	 * return false for error int for resource left
	 */
	public function getResLeftCnt( string $strLockName, int $iResID )
	{
		if( $iResID <= 0 ||
			!$strLockName ||
			( $strLockName = trim( $strLockName ) ) == '' )
		{
			return self::ERR_PARAMETERS;
		}
		
		$strLock = CachePrefixEnums::LOCK_RES_PREFIX .  $strLockName;
		
		$strScript = <<<EOS
		local strLeft = redis.call( 'hGet', '$strLock', '$iResID$this->LEFT_APPENDIX' );
		if strLeft then
			return strLeft;
		end	
		return false;
EOS;
		
		return $this->_redis->eval( $strScript );
	}
	
	/**
	 * 取初始资源总量
	 * @param $strLockName
	 * @param $iResID
	 * 
	 * return false for error others for success
	 */
	public function getResInitCnt( string $strLockName, int $iResID )
	{
		if( $iResID <= 0 ||
			!$strLockName ||
			( $strLockName = trim( $strLockName ) ) == '' )
		{
			return self::ERR_PARAMETERS;
		}
		
		$strLock = CachePrefixEnums::LOCK_RES_PREFIX .  $strLockName;
		
		$strScript = <<<EOS
		local strCnt = redis.call( 'hGet', '$strLock', $iResID );
		if strCnt then
			return strCnt;
		end
		return $this->ERR_NONE_INIT;
EOS;
		
		return $this->_redis->eval( $strScript );
	}
	
	/**
	 * 取资源使用情况
	 * 返回资源的初始值， 未消费资源量， 资源名， 资源ID
	 * 此接口主要用于前端显示规格之类的销售情况
	 * 
	 * return array for success false for error
	 */
	public function getResStatus( string $strLockName, int $iResID )
	{
		if( $iResID <= 0 ||
			!$strLockName ||
			( $strLockName = trim( $strLockName ) ) == '' )
		{
			return self::ERR_PARAMETERS;
		}
		
		$strLock = CachePrefixEnums::LOCK_RES_PREFIX .  $strLockName;
		
		$strScript = <<<EOS
		return redis.call( 'hMGet', '$strLock', $iResID, '$iResID$this->LEFT_APPENDIX' );
EOS;
	
		$arrRet = $this->_redis->eval( $strScript );
		if( is_array( $arrRet ) && $arrRet[ 0 ] )
		{
			return array( 'name' => $strLockName, 'id' => $iResID, 'max' => $arrRet[0], 'left' => $arrRet[1] );
		}
		
		return self::ERR_NONE_INIT;
	}
	
	/**
	 * @param string $strLockName 资源名
	 * @param int $iResID 资源ID
	 * @param int $iApplicantID 资源的申请者ID
	 * @param int $iRequestNum 需要的资源数量
	 * @param int $holdSeconds
	 * @return boolean
	 */
	public function lockRes( string $strLockName, int $iResID, int $iApplicantID, int $iRequestNum, int $iHoldSeconds = self::DEFAULT_LOCK_SECONDS )
	{
		/*
		 * 算法：此资源锁使用共使用了两类数据即hash与string共三组
	 	 * h_s_lock_{$strLockName}:锁 所有的资源锁的最大资源数存放于存hash中采用 如spec numbers规格数 其中的每一组 $iResID:资源数 即是该类的一组资源
	 	 * h_s_lock_res_{$strLockName}_{$iResID}:这里存放的是持锁请求者的ID,其中的每一项即为 $iApplicantID:$
		 */
		
		if( $iResID <= 0 ||
			$iApplicantID <= 0 ||
			$iRequestNum <= 0 ||
			$iHoldSeconds <= 0 ||
			!$strLockName ||
			( $strLockName = trim( $strLockName ) ) == '' )
		{
			return $this->ERR_PARAMETERS;//ERR_PARAMETERS = -1;
		}
		
		$strLockHash = CachePrefixEnums::LOCK_RES_PREFIX . $strLockName;//资源锁集
		$strListApplicantKeys = CachePrefixEnums::LOCK_RES_LIST_KEYS_PREFIX . $strLockName . '_' . $iResID;//用于获取某个资源所有的持锁者ID集
		$strApplicantKeyPrefix = CachePrefixEnums::LOCK_RES_ITEM_PREFIX . $strLockName . '_' . $iResID . '_';//资源持有者ID用于获取string
		
		$strScript = <<< EOS
		
		local strSum = redis.call( 'hGet', '$strLockHash', '$iResID$this->LEFT_APPENDIX' );
		local iSum = 0;
		
		if( strSum  ) then
			iSum = tonumber( strSum );
		else
			return $this->ERR_NONE_INIT;
		end
		
		local arrKeys = redis.call( 'lrange', '$strListApplicantKeys', 0, -1 );
		
		if( arrKeys and #arrKeys == 0 ) then
		
			if( iSum <= 0 ) then
				return $this->ERR_INSUFFICIENT; 
			elseif( $iRequestNum > iSum ) then
				return $this->ERR_EXCEED;	
			end
			
			redis.call( 'rpush', '$strListApplicantKeys', $iApplicantID );
			local strLockItem = '$strApplicantKeyPrefix' .. $iApplicantID;
			redis.call( 'set', strLockItem, $iRequestNum, 'EX', $iHoldSeconds );
			
			return $this->ERR_SUCCESS;
		else
			local arrStrKeys = {};
			local iKeySize = table.getn( arrKeys );
			local bFoundApplicant = false;
			local iLockRes = 0;
			
			for i = 1, iKeySize do
				if( tonumber( arrKeys[ i ] ) == $iApplicantID ) then
					local strLockRes = redis.call( 'get', '$strApplicantKeyPrefix' .. $iApplicantID );
					if( strLockRes and ( tonumber( strLockRes ) <= $iRequestNum )) then
						local strLockItem = '$strApplicantKeyPrefix' .. $iApplicantID;
						redis.call( 'set', strLockItem, $iRequestNum, 'EX', $iHoldSeconds );
						return $this->ERR_SUCCESS;
					end
					
					if strLockRes then
						iLockRes = tonumber( strLockRes );
					end
					bFoundApplicant = true;
				end
				
				table.insert( arrStrKeys, '$strApplicantKeyPrefix' .. arrKeys[ i ] );
			end 

			local arrRes = redis.call( 'mget', unpack( arrStrKeys ) );
			local iLocked = 0;
			local iArrRes = table.getn( arrRes );
			for i = 1, iArrRes do
				if( arrRes[ i ] ) then
					iLocked = iLocked + arrRes[ i ];
				end
			end
					
			if( iLocked - iLockRes + $iRequestNum <= iSum ) then
				if( not bFoundApplicant ) then
					redis.call( 'rpush', '$strListApplicantKeys', $iApplicantID );
				end
				redis.call( 'set', '$strApplicantKeyPrefix' .. $iApplicantID, $iRequestNum, 'EX', $iHoldSeconds );
				return $this->ERR_SUCCESS;
			end
						
			return $this->ERR_INSUFFICIENT; 
		end
	
EOS;

		return $this->_redis->eval( $strScript );
	}
	
	/**
	 * status: to be done
	 * @param string $strLockName
	 * @param int $iID
	 * 
	 * return true for success others for error
	 */
	public function unlockRes( string $strLockName, int $iResID, int $iApplicantID )
	{//删除list中的key清除string中相关的key
		
		if( $iResID <= 0 ||
			$iApplicantID <= 0 ||
			!$strLockName ||
			( $strLockName = trim( $strLockName ) ) == '' )
		{
			return $this->ERR_PARAMETERS;//ERR_PARAMETERS = -1;
		}
		
		$strListApplicantKeys = CachePrefixEnums::LOCK_RES_LIST_KEYS_PREFIX . $strLockName . '_' . $iResID;
		$strApplicantKey = CachePrefixEnums::LOCK_RES_ITEM_PREFIX . $strLockName . '_' . $iResID . '_' . $iApplicantID;
		
		$strScript = <<<EOS
		redis.call( 'lRem', '$strListApplicantKeys', 0, $iApplicantID );
		redis.call( 'del', '$strApplicantKey' );
		return true;
EOS;
		return $this->_redis->eval( $strScript );
	}
	
	/**
	 * 删除资源锁
	 * @param string $strLockName
	 * @param int $iResID
	 * @return number
	 */
	public function delLock( string $strLockName, int $iResID )
	{
		if( $iResID <= 0 ||
			!$strLockName ||
			( $strLockName = trim( $strLockName ) ) == '' )
		{
			return $this->ERR_PARAMETERS;//ERR_PARAMETERS = -1;
		}
		
		$strLock = CachePrefixEnums::LOCK_RES_PREFIX .  $strLockName;
		$strListApplicantKeys = CachePrefixEnums::LOCK_RES_LIST_KEYS_PREFIX . $strLockName . '_' . $iResID;
		$strApplicantKeyPrefix = CachePrefixEnums::LOCK_RES_ITEM_PREFIX . $strLockName . '_' . $iResID . '_';
		$strConsumeLog = CachePrefixEnums::LOCK_RES_LOCK_CONSUME_PREFIX . $iResID;//消费记录
		
		$strScript = <<<EOS
		local arrApps = redis.call( 'lrange', '$strListApplicantKeys', 0, -1 );
		local strKey = '';
		if( arrApps and #arrApps > 0 ) then
			local iSize = #arrApps;
			for i = 1, iSize do
				strKey = '$strApplicantKeyPrefix' .. arrApps[ i ];
				redis.call( 'del', strKey );
			end
			redis.call( 'del', '$strListApplicantKeys' );
		end
		redis.call( 'hDel', '$strLock', $iResID );
		redis.call( 'hDel', '$strLock', '$iResID$this->LEFT_APPENDIX' );
		redis.call( 'del', '$strConsumeLog' );
		return true;
EOS;
		return $this->_redis->eval( $strScript );
	}
	
	/**
	 * 消费资源 这里不会出现重复消费的情况
	 * 
	 * @param string $strLockName
	 * @param int $iResID
	 * @param int $iApplicantID
	 * 
	 * return true for success others for error
	 */
	public function consume( string $strLockName, int $iResID, int $iApplicantID )
	{
		if( $iResID <= 0 ||
			$iApplicantID <= 0 ||
			!$strLockName ||
			( $strLockName = trim( $strLockName ) ) == '' )
		{
			return $this->ERR_PARAMETERS;//ERR_PARAMETERS = -1;
		}
		
		$strLock = CachePrefixEnums::LOCK_RES_PREFIX .  $strLockName;
		$strListApplicantKeys = CachePrefixEnums::LOCK_RES_LIST_KEYS_PREFIX . $strLockName . '_' . $iResID;
		$strApplicantKey = CachePrefixEnums::LOCK_RES_ITEM_PREFIX . $strLockName . '_' . $iResID . '_' . $iApplicantID;
		$strConsumeLog = CachePrefixEnums::LOCK_RES_LOCK_CONSUME_PREFIX . $iResID;
		
		$strScript = <<<EOS
		
		local strAppNum = redis.call( 'get', '$strApplicantKey' );
		if strAppNum then
			local iAppNum = tonumber( strAppNum );
			redis.call( 'hIncrBy', '$strLock', '$iResID$this->LEFT_APPENDIX', -1 * iAppNum );
			redis.call( 'lRem', '$strListApplicantKeys', 0, $iApplicantID );
			redis.call( 'del', '$strApplicantKey' );
			redis.call( 'hSet', '$strConsumeLog', $iApplicantID, iAppNum );		
			return iAppNum;
		end
		
		return $this->ERR_CONSUME;
EOS;
		
		return $this->_redis->eval( $strScript );
	}
	
	/**
	 * 取资源申请者及其申请的资源量
	 * 
	 * @param string $strLockName
	 * @param int $iResID
	 * @return int for error object for success
	 */
	public function getApplicants( string $strLockName, int $iResID )
	{
		if( $iResID <= 0 ||
			!$strLockName ||
			( $strLockName = trim( $strLockName ) ) == '' )
		{
			return $this->ERR_PARAMETERS;//ERR_PARAMETERS = -1;
		}
		
		$strLock = CachePrefixEnums::LOCK_RES_PREFIX .  $strLockName;
		$strListApplicantKeys = CachePrefixEnums::LOCK_RES_LIST_KEYS_PREFIX . $strLockName . '_' . $iResID;
		$strApplicantKeyPrefix = CachePrefixEnums::LOCK_RES_ITEM_PREFIX . $strLockName . '_' . $iResID . '_';
		
		$strScript = <<< EOS
		
			local arrKeys = redis.call( 'lrange', '$strListApplicantKeys', 0, -1 );
			if( not arrKeys or #arrKeys == 0 ) then
				return $this->ERR_NO_APPLICANTS;
			end

			local arrStrKeys = {};
			local iKeySize = table.getn( arrKeys );
						
			for i = 1, iKeySize do
				table.insert( arrStrKeys, '$strApplicantKeyPrefix' .. arrKeys[ i ] );
			end 
			local arrRes = redis.call( 'mget', unpack( arrStrKeys ) );	
			local arrRets = {};
			for i = iKeySize, 1, -1 do
				if( arrRes[i] ) then
					arrRets[ arrKeys[i] ] = arrRes[i];
				else
					redis.call( 'lRem', '$strListApplicantKeys', 0, arrKeys[i] );
					redis.call( 'del', '$strApplicantKeyPrefix .. arrKeys[i]' );
				end
			end
							
			if( next( arrRets )  ) then
				return cjson.encode( arrRets );
			else
				return $this->ERR_NO_APPLICANTS;
			end
EOS;

		
		$ret = $this->_redis->eval( $strScript );
		if( $ret && is_string( $ret ))
		{
			return json_decode( $ret );
		}
		
		return $ret;
	}
	
	/**
	 * 剩余的未加锁的资源与剩余的资源总量
	 * @param $strLockName
	 * @param $iResID
	 * 
	 * array for success int for error e.g. 
	 * object( public 'locked' => 11, public 'left' => '17' )
	 */
	public function getNoneLockedRes( string $strLockName, int $iResID )
	{
		if( $iResID <= 0 ||
			!$strLockName ||
			( $strLockName = trim( $strLockName ) ) == '' )
		{
			return $this->ERR_PARAMETERS;//ERR_PARAMETERS = -1;
		}
		
		$strLock = CachePrefixEnums::LOCK_RES_PREFIX .  $strLockName;
		$strListApplicantKeys = CachePrefixEnums::LOCK_RES_LIST_KEYS_PREFIX . $strLockName . '_' . $iResID;
		$strApplicantKeyPrefix = CachePrefixEnums::LOCK_RES_ITEM_PREFIX . $strLockName . '_' . $iResID . '_';
		
		$strScript = <<< EOS
			local strLeft = redis.call( 'hGet', '$strLock', '$iResID$this->LEFT_APPENDIX' );
			if not strLeft then
				return $this->ERR_NONE_INIT; 
			end
			
			local arrRets = {};
			
			local arrKeys = redis.call( 'lrange', '$strListApplicantKeys', 0, -1 );
			if( not arrKeys or #arrKeys == 0 ) then
				
				arrRets['locked'] = 0;
				arrRets['left'] = strLeft;
				
				return cjson.encode( arrRets );
			end

			local arrStrKeys = {};
			local iKeySize = table.getn( arrKeys );
						
			for i = 1, iKeySize do
				table.insert( arrStrKeys, '$strApplicantKeyPrefix' .. arrKeys[ i ] );
			end 
			
			local arrRes = redis.call( 'mget', unpack( arrStrKeys ) );	
			local iSum = 0;
			for i = iKeySize, 1, -1 do
				if( arrRes[i] ) then
					iSum = iSum + tonumber( arrRes[i] );
				else
					redis.call( 'lRem', '$strListApplicantKeys', 0, arrKeys[i] );
					redis.call( 'del', '$strApplicantKeyPrefix .. arrKeys[i]' );
				end
			end
			
			arrRets[ 'locked' ] = iSum;
			arrRets[ 'left' ] = tonumber( strLeft ); 
							
			return cjson.encode( arrRets );
EOS;

		
		$ret = $this->_redis->eval( $strScript );
		if( $ret && is_string( $ret ))
		{
			return json_decode( $ret );
		}
		
		return $ret;
	}
	
	/**
	 * 退款后需要回滚Res
	 * @param string $strLockName
	 * @param int $iResID
	 * @param int $iApplicantID
	 * @param int $iCnt 0:退回所有已消费的， >0 退回指定数目已消费的
	 * 
	 * return -1:参数错误 false:错误 true for success 
	 */
	public function incrRes( string $strLockName, int $iResID, int $iApplicantID, int $iCnt = 0 )
	{
		if( $iResID <= 0 ||
			$iCnt < 0 ||
			$iApplicantID <= 0 || 
			!$strLockName ||
			( $strLockName = trim( $strLockName ) ) == '' )
		{
			return $this->ERR_PARAMETERS;//ERR_PARAMETERS = -1;
		}
		
		$strConsumeLog = CachePrefixEnums::LOCK_RES_LOCK_CONSUME_PREFIX . $iResID;
		$strLock = CachePrefixEnums::LOCK_RES_PREFIX .  $strLockName;
		
		/*
		 * 为了性能做了一些妥协： script中本应该使用两个变量转而使用了一个变量iConsumedNum来代替两个变量的功能特此说明
		 */
		$strScript = <<<EOS
		local strConsumedNum = redis.call( 'hGet', '$strConsumeLog', $iApplicantID );
		
		if strConsumedNum then
			local iConsumedNum = tonumber( strConsumedNum );
			if $iCnt > iConsumedNum then
				return false;
			end
					
			if 0 ~= $iCnt then
				iConsumedNum = $iCnt;
			end
			
			redis.call( 'hIncrBy', '$strConsumeLog', $iApplicantID, -1 * iConsumedNum );
			redis.call( 'hIncrBy', '$strLock', '$iResID$this->LEFT_APPENDIX', iConsumedNum );

			return true;
		end
		
		return false;
EOS;
		
		$iRet = $this->_redis->eval( $strScript );
		
		if( $iRet )
		{
			return true;
		}
		
		return false;
	}

}